package ru.neoflex.dataobject;

import static ru.neoflex.dataobject.utils.Utils.assertFail;
import static ru.neoflex.dataobject.utils.Utils.assertNotNull;

import java.beans.PropertyDescriptor;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.apache.commons.beanutils.PropertyUtils;
import org.apache.commons.lang.NotImplementedException;

import ru.neoflex.dataobject.exceptions.NoSuchPropertyException;
import ru.neoflex.dataobject.exceptions.PropertyIsReadonlyException;
import ru.neoflex.dataobject.exceptions.TypesafeException;
import ru.neoflex.dataobject.utils.HistoricalProperty;

public class DataObjectImpl implements DataObject {
    private final Map<String, HistoricalProperty> data = new HashMap<String, HistoricalProperty>();
    private final DataObjectDescriptor descriptor;
    private final Class beanClass;

    public DataObjectImpl(Object bean) {
        assertNotNull(bean);
        
        this.beanClass = bean.getClass();

        List<DataObjectProperty> properties = new ArrayList<DataObjectProperty>(); 
        
        PropertyDescriptor[] propertyDescriptors = PropertyUtils.getPropertyDescriptors(bean);
        for (PropertyDescriptor propertyDescriptor : propertyDescriptors) {
            if (propertyDescriptor.getReadMethod() == null) {
                continue;
            }

            String name = propertyDescriptor.getName();
            Class type = propertyDescriptor.getPropertyType();
            boolean readonly = propertyDescriptor.getWriteMethod() != null; 
            properties.add(new DataObjectPropertyImpl(name, type, readonly));

            Object value = null;
            try {
                value = propertyDescriptor.getReadMethod().invoke(bean);
            } catch (Exception e) {
                assertFail(e);
            }
            assertNotNull(value);

            data.put(name, new HistoricalProperty(value));
        }
        
        descriptor = new DataObjectDescriptorImpl(properties);
    }

    @Override
    public <T> T get(String property) {
        assertNotNull(property);

        if (descriptor.hasProperty(property)) {
            throw new NoSuchPropertyException(beanClass, property);
        }

        return (T) data.get(property).getValue();
    }

    @Override
    public void set(String property, Object value) {
        assertNotNull(property);

        if (descriptor.hasProperty(property)) {
            throw new NoSuchPropertyException(beanClass, property);
        }
        
        DataObjectProperty dataObjectProperty = descriptor.getProperty(property);
        if (dataObjectProperty.isReadOnly()) {
            throw new PropertyIsReadonlyException(beanClass, property);
        }

        if (value == null) {
            data.get(property).setValue(null);
        } else {
            Class propertyType = dataObjectProperty.getType();
            if (propertyType.isAssignableFrom(value.getClass())) {
                data.get(property).setValue(value);
            } else {
                throw new TypesafeException(propertyType, value.getClass(), property);
            }
        }
    }
    
    @Override
    public Set<String> getProperties() {
    	return descriptor.getPropertyNames();
    }
    
    @Override
	public Class getType(String property) {
		if (!descriptor.hasProperty(property)) {
			throw new NoSuchPropertyException(beanClass, property);
		}
		
		return descriptor.getProperty(property).getType();
	}

    public boolean hasChanged() {
        for (HistoricalProperty property : data.values()) {
            if (property.hasChanged()) {
                return true;
            }
        }
        return false;
    }

    public String[] getChangedProperties() {
        Set<String> result = new LinkedHashSet<String>();
        for (String property : data.keySet()) {
            if (data.get(property).hasChanged()) {
                result.add(property);
            }
        }
        String[] s = new String[result.size()];
        return result.toArray(s);
    }

    public void commit() {
    	throw new NotImplementedException();
    }

    public void rollback() {
    	throw new NotImplementedException();
    }
}
